/*
 * Copyright 2010, Andrew M Gibson
 *
 * www.andygibson.net
 *
 * This file is part of DataValve.
 *
 * DataValve is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * DataValve is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with DataValve.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.fluttercode.datavalve.provider;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.fluttercode.datavalve.Paginator;
import org.fluttercode.datavalve.provider.util.DataQuery;
import org.fluttercode.datavalve.provider.util.DataQueryBuilder;

/**
 * Abstract class for Query driven datasets that implements most of
 * the methods in the {@link QueryDataProvider} interface.
 * It adds features for defining select/count statements, and translating
 * orderKey values into other representations. It also defines common methods
 * for defining an order clause and building statements based on the Sql/Ejbql
 * structure.
 *
 * Typically, the orderKey translates to an order value through the OrderKeyMap.
 * However, we wrap this behaviour in the {@link #translateOrderKey(String)}
 * method. This can be overridden if you want to change how we translate
 * orderKey values.
 *
 * @author Andy Gibson
 *
 * @param <T> Type of object this dataset contains.
 */
public abstract class AbstractQueryDataProvider<T> extends AbstractQLDataProvider<T> implements QueryDataProvider<T>, Serializable {

    private static final long serialVersionUID = 1L;

    private int paramId;
    private Map<String, String> orderKeyMap = new HashMap<String, String>();
    private List<String> restrictions = new ArrayList<String>();
    private boolean orderedParams = false;

    @Override
    public Map<String, String> getOrderKeyMap() {
        return orderKeyMap;
    }

    @Override
    public void setOrderKeyMap(Map<String, String> orderKeyMap) {
        this.orderKeyMap = orderKeyMap;
    }

    @Override
    public List<String> getRestrictions() {
        return restrictions;
    }

    @Override
    public void setRestrictions(List<String> restrictions) {
        this.restrictions = restrictions;
    }

    public boolean isOrderedParams() {
        return orderedParams;
    }

    public void setOrderedParams(boolean orderedParams) {
        this.orderedParams = orderedParams;
    }

    @Override
    public void init(Class<? extends Object> clazz, String prefix) {
        setCountStatement(String.format("select count(%s) from %s %s ", prefix,
                clazz.getSimpleName(), prefix));
        setSelectStatement(String.format("select %s from %s %s ", prefix,
                clazz.getSimpleName(), prefix));
    }

    /**
     * Returns the order fields for a given order key pair.
     * Default implementation works off the
     * {@link AbstractQueryDataProvider#orderKeyMap} {@link Map} property.
     *
     * Can be overridden to handle custom specific mappings.
     *
     * @param orderKeyValue
     * @return
     */
    protected String translateOrderKey(String orderKeyValue) {
        return getOrderKeyMap().get(orderKeyValue);
    }

    protected String getNextParamName() {
        return "_param_" + String.valueOf(paramId++);
    }

    @Override
    public void addRestriction(String restriction) {
        getRestrictions().add(restriction);
    }

    @Override
    public boolean addRestriction(String syntax, Object value) {
        return addRestriction(syntax, value, value);
    }

    @Override
    public boolean addRestrictionStr(String syntax, String value) {
        return addRestrictionStr(syntax, value, value);
    }

    @Override
    public boolean addRestrictionStr(String syntax, String paramValue, String testValue) {
        if (isValidTestValue(testValue, true)) {
            return addRestriction(syntax, paramValue, testValue);
        }
        return false;
    }

    @Override
    public boolean addRestriction(String syntax, Object paramValue, Object testValue) {
        if (isValidTestValue(testValue, false)) {
            if (paramValue instanceof Collection<?>) {
                addCollectionRestriction(syntax, (Collection<?>) paramValue);
            } else {
                addRestrictionForDefault(syntax, paramValue);
            }
            return true;
        }
        return false;
    }

    /**
     * Adds a restriction based on a collection. It is assumed that the syntax
     * used is acceptable for a collection, for example, using
     * <code> field in (:param)</code> and passing in a collection.
     *
     * For each item in the collection, see if it is valid and if so, add it to
     * the list of parameters to be used in the statement.
     *
     * @param syntax syntax to use in the restriction
     * @param paramValue
     */
    private void addCollectionRestriction(String syntax, Collection<?> paramValues) {
        StringBuilder paramList = new StringBuilder();
        for (Object o : paramValues) {
            if (isValidTestValue(o, false)) {
                // prefix with comma if its not the first
                if (paramList.length() != 0) {
                    paramList.append(",");
                }
                String name = getNextParamName();
                paramList.append(":").append(name);
                getParameters().put(name, o);
            }
        }
        syntax = syntax.replace(":param", paramList.toString());
        addRestriction(syntax);
    }

    /**
     * Adds a restriction based on a parameter value that is expected to be a
     * typical value (String, Long, int, boolean, etc) instead of a more exotic
     * parameter type such as as collection.
     * This method assumes any pre-add checks have been done for null params or test params etc,
     *
     * @param syntax Syntax of the restriction
     * @param paramValue Value to use
     */
    private void addRestrictionForDefault(String syntax, Object paramValue) {
        String name = getNextParamName();
        syntax = syntax.replace(":param", ":" + name);
        addRestriction(syntax);
        getParameters().put(name, paramValue);
    }

    protected boolean isValidTestValue(Object obj, boolean testStrings) {
        if (obj == null) {
            return false;
        }
        if (obj instanceof String && testStrings) {
            String s = (String) obj;
            return s.length() != 0;
        }
        if (obj instanceof Collection<?>) {
            if (((Collection<?>) obj).isEmpty()) {
                return false;
            }
            // check it contains a valid test value
            for (Object o : (Collection<?>) obj) {
                // avoid recursion by checking for self
                if (o != obj) {
                    if (isValidTestValue(o, testStrings)) {
                        return true;
                    }
                }
            }
        }
        return true;
    }

    /**
     * Constructs and returns a {@link DataQueryBuilder} instance.
     * Override this to return alternative query builders that have different
     * functions or to change attributes in the one returned by default.
     *
     * @return A DataQueryBuilder to use for creating the {@link DataQuery}
     */
    protected DataQueryBuilder createDataQueryBuilder() {
        return new DataQueryBuilder();
    }

    @Override
    protected DataQuery buildDataQuery(String baseStatement, boolean includeOrdering, Paginator paginator) {
        DataQueryBuilder builder = new DataQueryBuilder();
        builder.setProvider(this);
        builder.setBaseStatement(baseStatement);
        builder.setOrderedParams(orderedParams);
        if (includeOrdering) {
            builder.setOrderBy(translateOrderKey(paginator.getOrderKey()));
            builder.setOrderAscending(paginator.isOrderAscending());
        }
        builder.setSuffix(getSuffix());
        return builder.build();
    }
}
